#!/bin/bash
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

usage () {
    echo Usage: `basename $0` "[-h|-u] [-d doclet] [-j javadoc] [-p user] [-s sdk] [-t task] [--] prior soon";
}

help () {
    usage
    cat <<EOF

Prepare commits for pushing to Gerrit, two commits for each Android Java package
within the git module, that express the interesting changes in API between a prior
release (tag or branch) and an imminent release (branch).

The script looks for various Java sources under the qt5 super repo path and generates
API descriptions for specific package names provided here, these are pre-defined Qt
for Android package names. This process is done for the prior Qt release and it's
committed as a base to-be-ignored commit. The same is done for Qt release being
reviewed and a commit is created on top.

The script can push commits to Gerrit, and automatically abondon the base commits.
Also, it can update existing API review patches, given that the local git branch
that was used to push before still exists with valid commits.

Optionally a Jira task number can be provided.

 -h
 --help     Print this help and exit.

-p USER
--push-as USER
            Push the review commits to Gerrit under the provided username.
            If this is not provided, commits won't be pushed.

-s PATH
--platform-jar PATH
            The Android SDK platform JAR that's used to compile the Java
            source code. This is typically the same as the maximum
            supported Android version for a given Qt release.

 -t ID
 --task ID
 --task-number ID
           Include a Task-number: in the commit message, with the
           given id, ID, of the bug-tracker issue being used to keep
           track of the API change review.
 -u
 --usage   Print simple usage summary (first line above) and exit.

 --        End option processing.

Arguments:

 prior     The commit (branch, tag or sha1) of a prior release.
 soon      The branch approaching release.

EOF
}

warn () { echo "$@" >&2; }
die () { warn "$@"; exit 1; }
second () {
    if [ $# -lt 2 ]
    then die "No argument supplied for $1"
    elif [ -z "$2" ]
    then die "Empty argument passed for $1 "
    fi
    echo "$2"
}

PLATFORM_JAR=
GERRIT_USER=
TASK_NUMBER=

bad () { usage >&2; die "$@"; }
while [ $# -gt 0 ]
do  case "$1" in
        -u|--usage) usage; exit 0 ;;
        -h|--help) help; exit 0 ;;
        -p|--push-as) GERRIT_USER=`second "$@"`; shift 2 ;;
        -s|--platform-jar) PLATFORM_JAR=`second "$@"`; shift 2 ;;
        -t|--task|--task-number) TASK_NUMBER=`second "$@"`; shift 2 ;;
        --) shift; break ;;
        -*) bad "Unrecognised option: $1" ;;
        *) break ;;
    esac
done

# Check basic expectations of context:
[ -f init-repository ] || \
    die "I expect to be run in the top level directory of the qt5 module (see --help)."
QT_SUPER_REPO="$(pwd)"

DOCLET_PATH=$QT_SUPER_REPO/qtqa/scripts/api-review/src

REVIEW_FILENAME=qt-java-api-signature.txt

# Select revisions to compare:
[ $# -eq 2 ] || bad "Expected exactly two arguments, got $#: $@"
for arg
do git rev-parse "$arg" -- >/dev/null || bad "Failed to parse $arg as a git ref"
done
PRIOR="$1"
RELEASE="$2"

check_file_param() {
    [ -n "$1" ] || die "$2 not provided (see --help)."
    [ -f "$1" ] || die "$2 path '$1' doesn't exist."
}

check_file_param "$PLATFORM_JAR" "Android Platform JAR"
command -v javac &> /dev/null || die "javac command not found (set JAVA_HOME)."
command -v javadoc &> /dev/null || die "javadoc command not found (set JAVA_HOME)."

compile_doclet() {
    javac $DOCLET_PATH/org/qtproject/qt/android/JavaApiSignature.java
}

run_javadoc() {
    source_paths="$1"
    version="$2"
    output_dir="$3"
    shift 3
    java_packages=("${@#*:}")

    echo $output_dir
    pushd $output_dir

    javadoc \
        -doclet org.qtproject.qt.android.JavaApiSignature \
        -docletpath $DOCLET_PATH \
        --output ${REVIEW_FILENAME} \
        --class-path $PLATFORM_JAR \
        -sourcepath $source_paths \
        "${java_packages[@]}" 2>&1 1>/dev/null | \
            # Ignore the five-line warning starting with this is line.
            sed '/javadoc: warning - The old Doclet and Taglet APIs/{N;N;N;N;d;}'

    mv $REVIEW_FILENAME $version-$REVIEW_FILENAME

    popd
}

run_javadoc_for_all_repos() {
    version="$1"
    repos=("${@:2}")

    java_source_paths=$(
        find "${repos[@]}" -type d -path '*/src/*/org/qtproject/qt/android' -exec echo {} + | \
            sed 's/\/org\/qtproject\/qt\/android[^ ]*//g' | sed 's/:$//' | tr ' ' ':'
    )

    echo "############################################"
    printf "####    Generating JAVADOC for %-8s ####\n" $version
    echo "############################################"
    for index in "${!repo_packages[@]}"; do
        entry="${repo_packages[$index]}"
        repo="${entry%%:*}"
        packages="${entry#*:}"
        run_javadoc $java_source_paths $version "$QT_SUPER_REPO/$repo" $packages
    done
    echo
}

checkout_git_repos() {
    branch="$1"
    repos=("${@:2}")

    echo "Checking out $branch in Qt repositories"
    for repo in "${repos[@]}"; do
        echo "Repo: $repo"
        git -C "$repo" fetch
        git -C "$repo" checkout -q $branch
    done
    echo
}

commit_api_verion() {
    message="$1"
    body="$2"
    ticket="$3"
    change_id="$4"
    version="$5"

    success=0
    head_commit_count="$(git rev-list --count HEAD...$version)"
    if [[ "$(ls -A $version-$REVIEW_FILENAME)" || $head_commit_count -eq 1 ]]; then
        mv $version-$REVIEW_FILENAME $REVIEW_FILENAME

        if [ "$(git status | grep $REVIEW_FILENAME)" ]; then
            echo "Committing API for $version..."
            git add $REVIEW_FILENAME
            if [[ -n $ticket ]]; then
                footer=$(echo -e "Task-number: $ticket\n$change_id")
                git commit -q -m "$message" -m "$body" -m "$footer"
            else
                git commit -q -m "$message" -m "$body" -m "$change_id"
            fi
            success="$?"
        else
            echo "No API changes to commit for $version..."
            git reset HEAD^
            rm $REVIEW_FILENAME
            success="-1"
        fi
    else
        echo "No API files for $version!"
        rm $version-$REVIEW_FILENAME
    fi

    return $success
}

commit_diff() {
    repo="$1"

    # Checkout review branch, remove it if it already exists
    echo "Preparing API Review Diff for $repo"
    cd "$repo"
    review_branch_name="java_api_review_${RELEASE}"

    # Get previous Change-Id if a review branch was already created
    if [ -n "$(git branch --list | grep -w $review_branch_name)" ]; then
        change_ids_log=$(git log --reverse "$RELEASE..$review_branch_name" | \
                     grep  "Change-Id: " | sed 's/^[ \t]*//')
        while IFS= read -r line; do change_ids+=("$line"); done <<< "$change_ids_log"
    fi

    echo "Checking out review branch..."
    git checkout -q $RELEASE
    git branch -q -D $review_branch_name
    git checkout -q -b $review_branch_name $RELEASE

    if [ -f "$REVIEW_FILENAME" ]; then
        rm $REVIEW_FILENAME
    fi

    module=$(basename $repo)
    commit_msg="WIP: [Ignore][API Base] Review $module $RELEASE Android Java API"
    body="Auto-generated $PRIOR baseline commit for reviewing $RELEASE, please ignore."
    if [[ -n "${change_ids[1]}" ]]; then
        prior_change_id="${change_ids[0]}"
        release_change_id="${change_ids[1]}"
    else
        prior_change_id=""
        release_change_id="${change_ids[0]}"
    fi

    commit_api_verion "$commit_msg" "$body" "" "$prior_change_id" "$PRIOR"

    if [ -d "$review_dir_base" ]; then
        rm -R $review_dir_base
    fi

    commit_msg="Review $module $RELEASE Android Java API"
    body="Auto-generated commit for reviewing the Android Java API of $module."
    commit_api_verion "$commit_msg" "$body" "$TASK_NUMBER" "$release_change_id" "$RELEASE"

    if [ $? -eq 0 ]; then
        echo "Check the API diff file under $module/$REVIEW_FILENAME"
    fi
}

do_gerrit_command() {
    ssh -p 29418 $GERRIT_USER@codereview.qt-project.org gerrit "$@"
}

push_to_gerrit() {
    version="$1"

    echo "Pushing changes to Gerrit..."
    git push gerrit HEAD:refs/for/$version

    # Set diff review patch topic
    do_gerrit_command set-topic $(git log -n 1 --skip=0 --pretty=format:%H) \
        --topic "java_api_review_$version"
}

is_valid_repo_dir() {
    local dir="$1"

    [ -d "$dir" ] || return 1 # must be an existing dir
    [ "$(ls -A "$dir" 2>/dev/null)" ] || return 1 # must not be empty
    git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1 # must be a git dir

    return 0
}

# Individual key-value assignments for the associative array
repo_packages_map=(
    "qtbase:org.qtproject.qt.android \
        org.qtproject.qt.android.bindings \
        org.qtproject.qt.android.extras \
        org.qtproject.qt.android.network \
        org.qtproject.qt.android.networkinformation"
    "qtconnectivity:org.qtproject.qt.android.bluetooth \
        org.qtproject.qt.android.nfc"
    "qtdeclarative:org.qtproject.qt.android"
    "qtmultimedia:org.qtproject.qt.android.multimedia"
    "qtpositioning:org.qtproject.qt.android.positioning"
    "qtspeech:org.qtproject.qt.android.speech"
    "qtwebview:org.qtproject.qt.android.view"
)

# Get list of absolute paths for git repos
repos=()
repo_packages=()
for entry in "${repo_packages_map[@]}"; do
    repo="${entry%%:*}"
    dir="$QT_SUPER_REPO/$repo"

    if is_valid_repo_dir "$dir"; then
        repos+=("$dir")
        repo_packages+=("$entry")
    else
        echo "Skipping '$dir': not a valid git repo" >&2
    fi
done

[[ ${#repos[@]} -eq 0 ]] && die "No valid git repositories found, nothing to do here."

compile_doclet

# Generate API for prior version
checkout_git_repos "$PRIOR" "${repos[@]}"
run_javadoc_for_all_repos $PRIOR "${repos[@]}"

# Generate API for release version
checkout_git_repos "$RELEASE" "${repos[@]}"
run_javadoc_for_all_repos $RELEASE "${repos[@]}"

# Commit diffs and push
for repo in "${repos[@]}"; do
    commit_diff $repo

    commit_count="$(git rev-list --count HEAD...$RELEASE)"
    if [[ $commit_count -gt 0 ]] && [[ -n $GERRIT_USER ]]; then
        if [ $commit_count -eq 2 ]; then
            # Restore base API commit if it exists before pushing
            echo "Restore base patch if exists"
            base_commit_change_id=$(git show --summary HEAD~1 | grep  "Change-Id: " | \
                                    sed 's/^[ \t]*//' | cut -d' ' -f2-)
            do_gerrit_command review --restore $base_commit_change_id
        fi

        push_to_gerrit $RELEASE

        if [ $commit_count -eq 2 ]; then
            # Abandon base API commit if it exists
            base_commit_commit_id=$(git log -n 1 --skip=1 --pretty=format:%H)
            echo "Abandoning remote commit $base_commit_commit_id..."
            do_gerrit_command review --abandon $base_commit_commit_id
        fi
    fi
    echo
done
